/*
 * See Licensing and Copyright notice in naev.h
 */
/**
 * @file land.c
 *
 * @brief Handles all the landing menus and actions.
 */
/** @cond */
#include "physfs.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#include "naev.h"
/** @endcond */

#include "land.h"

#include "array.h"
#include "camera.h"
#include "conf.h"
#include "dialogue.h"
#include "economy.h"
#include "equipment.h"
#include "event.h"
#include "gui.h"
#include "gui_omsg.h"
#include "hook.h"
#include "land_outfits.h"
#include "land_shipyard.h"
#include "land_trade.h"
#include "log.h"
#include "map.h"
#include "menu.h"
#include "mission.h"
#include "music.h"
#include "ndata.h"
#include "news.h"
#include "nlua.h"
#include "nlua_spfx.h"
#include "nlua_tk.h"
#include "npc.h"
#include "nstring.h"
#include "ntime.h"
#include "ntracing.h"
#include "player.h"
#include "player_autonav.h"
#include "player_fleet.h"
#include "rng.h"
#include "save.h"
#include "sound.h"
#include "toolkit.h"

/*
 * we use visited flags to not duplicate missions generated
 */
#define VISITED_LAND ( 1 << 0 ) /**< Player already landed. */
#define VISITED_COMMODITY                                                      \
   ( 1 << 1 )                        /**< Player already visited commodities.  \
                                      */
#define VISITED_BAR ( 1 << 2 )       /**< Player already visited bar. */
#define VISITED_OUTFITS ( 1 << 3 )   /**< Player already visited outfits. */
#define VISITED_SHIPYARD ( 1 << 4 )  /**< Player already visited shipyard. */
#define VISITED_EQUIPMENT ( 1 << 5 ) /**< Player already visited equipment. */
#define VISITED_MISSION                                                        \
   ( 1 << 6 ) /**< Player already visited mission computer. */
#define visited( f ) ( land_visited |= ( f ) ) /**< Mark place is visited. */
#define has_visited( f )                                                       \
   ( land_visited & ( f ) )           /**< Check if player has visited. */
static unsigned int land_visited = 0; /**< Contains what the player visited. */

/* Which tabs have been generated by their respective open functions. */
unsigned int land_generated = 0;

/*
 * land variables
 */
int        landed       = 0; /**< Is player landed. */
int        land_loaded  = 0; /**< Finished loading? */
static int land_takeoff = 0; /**< Takeoff. */
static int land_takeoff_nosave =
   0; /**< Whether or not to disable saving when taking off. */
unsigned int land_wid   = 0; /**< Land window ID, also used in gui.c */
static int   land_regen = 0; /**< Whether or not regenning. */
static int   land_windowsMap[LAND_NUMWINDOWS]; /**< Mapping of windows. */
static unsigned int *land_windows = NULL;      /**< Landed window ids. */
Spob                *land_spob    = NULL;      /**< Spob player landed at. */
static glTexture    *gfx_exterior =
   NULL; /**< Exterior graphic of the landed spob. */

/*
 * mission computer stack
 */
static Mission *mission_computer = NULL; /**< Missions at the computer. */
static int     *mission_computer_filter =
   NULL; /**< IDs of the filtered missions. */
MissionComputerOptions misn_opts;

/*
 * Bar stuff.
 */
static glTexture *mission_portrait = NULL; /**< Mission portrait. */

/*
 * player stuff
 */
static int last_window = 0; /**< Default window. */

/*
 * Error handling.
 */
static char  errorlist[STRMAX_SHORT];
static char  errorreason[STRMAX_SHORT];
static int   errorappend;
static char *errorlist_ptr;

/*
 * Rescue.
 */
static nlua_env *rescue_env = NULL; /**< Rescue Lua env. */
static void      land_stranded( void );

/*
 * prototypes
 */
static int  land_gc( void *unused );
static int  land_hasLocalMap( void );
static void land_createMainTab( unsigned int wid );
static void land_setupTabs( void );
static void land_cleanupWindow( unsigned int wid, const char *name );
static void land_changeTab( unsigned int wid, const char *wgt, int old,
                            int tab );
/* spaceport bar */
static void bar_getDim( int wid, int *w, int *h, int *iw, int *ih, int *bw,
                        int *bh );
static void bar_open( unsigned int wid );
static int  bar_genList( unsigned int wid );
static void bar_update( unsigned int wid, const char *str );
static void bar_close( unsigned int wid, const char *str );
static void bar_approach( unsigned int wid, const char *str );
static int  news_load( void );
/* mission computer */
static void misn_popdown( unsigned int wid, const char *str );
static void misn_popdownActivate( unsigned int wid, const char *str );
static void misn_popdownSelect( unsigned int wid, const char *str );
static void misn_computerOptions( unsigned int wid, const char *str );
static void misn_computerOptionsRegen( unsigned int wid, const char *str );
static void misn_computerOptionsClose( unsigned int wid, const char *name );
static void misn_open( unsigned int wid );
static void misn_autonav( unsigned int wid, const char *str );
static void misn_accept( unsigned int wid, const char *str );
static void misn_genList( unsigned int wid );
static void misn_update( unsigned int wid, const char *str );
static void misn_popdown( unsigned int wid, const char *str );

/**
 * @brief Queue a takeoff.
 */
void land_queueTakeoff( void )
{
   land_takeoff        = 1;
   land_takeoff_nosave = player_isFlag( PLAYER_NOSAVE );
}

void land_needsTakeoff( int delay )
{
   if ( land_takeoff )
      takeoff( delay, land_takeoff_nosave );
}

/* Maps are only offered if the spob provides fuel. */
static int land_hasLocalMap( void )
{
   if ( !spob_hasService( land_spob, SPOB_SERVICE_REFUEL ) )
      return 0;
   return 1;
}

/**
 * @brief Whether or not the player can save.
 */
int land_canSave( void )
{
   /* Has to be landed. */
   if ( !landed )
      return 0;

   /* Overrided case. */
   if ( player_isFlag( PLAYER_NOSAVE ) )
      return 0;

   /* If the current landed planet is refuelable, no need to check if can land.
    */
   if ( ( land_spob != NULL ) &&
        spob_hasService( land_spob, SPOB_SERVICE_REFUEL ) )
      return 1;

   /* For other places we'll have to see if can land. */
   for ( int i = 0; i < array_size( cur_system->spobs ); i++ ) {
      Spob *p = cur_system->spobs[i];
      spob_updateLand( p );
      if ( spob_hasService( p, SPOB_SERVICE_REFUEL ) && p->can_land )
         return 1;
   }
   return 0;
}

/**
 * @brief Check to see if finished loading.
 */
int land_doneLoading( void )
{
   if ( landed && land_loaded )
      return 1;
   return 0;
}

/**
 * @brief Clear error dialogues.
 */
void land_errClear( void )
{
   errorlist_ptr = NULL; /* Clear errors. */
}

/**
 * @brief Generates error dialogues used by several landing tabs.
 *    @param fmt String with printf-like formatting
 */
void land_errDialogueBuild( const char *fmt, ... )
{
   va_list ap;

   if ( fmt == NULL )
      return;
   va_start( ap, fmt );
   vsnprintf( errorreason, sizeof( errorreason ), fmt, ap );
   va_end( ap );

   if ( errorlist_ptr == NULL ) /* Initialize on first run. */
      errorappend =
         scnprintf( errorlist, sizeof( errorlist ), "%s", errorreason );
   else /* Append newest error to the existing list. */
      scnprintf( &errorlist[errorappend], sizeof( errorlist ) - errorappend,
                 "\n%s", errorreason );
   errorlist_ptr = errorlist;
}

/**
 * @brief Displays an error if applicable.
 */
int land_errDisplay( void )
{
   if ( errorlist_ptr != NULL ) {
      dialogue_alert( "%s", errorlist );
      return 1;
   }
   return 0;
}

/**
 * @brief Gets the dimensions of the spaceport bar window.
 */
static void bar_getDim( int wid, int *w, int *h, int *iw, int *ih, int *bw,
                        int *bh )
{
   /* Get window dimensions. */
   window_dimWindow( wid, w, h );

   /* Calculate dimensions of portraits. */
   *iw = 704 + ( *w - LAND_WIDTH );
   *ih = *h - 60;

   /* Calculate button dimensions. */
   *bw = MIN( LAND_BUTTON_WIDTH, ( *w - *iw - 80 ) / 2 );
   *bh = LAND_BUTTON_HEIGHT;
}
/**
 * @brief Opens the spaceport bar window.
 */
static void bar_open( unsigned int wid )
{
   int         w, h, iw, ih, bw, bh, dh, th;
   const char *desc;

   /* Mark as generated. */
   land_tabGenerate( LAND_WINDOW_BAR );

   /* Set window functions. */
   window_onClose( wid, bar_close );

   /* Get dimensions. */
   desc = ( land_spob->bar_description != NULL )
             ? _( land_spob->bar_description )
             : "(NULL)";
   bar_getDim( wid, &w, &h, &iw, &ih, &bw, &bh );
   dh = gl_printHeightRaw( &gl_defFont, w - iw - 60, desc );

   /* Approach when pressing enter */
   window_setAccept( wid, bar_approach );

   /* Buttons */
   window_addButtonKey( wid, -20, 20, bw, bh, "btnCloseBar", _( "Take Off" ),
                        land_buttonTakeoff, SDLK_T );
   window_addButtonKey( wid, -20 - bw - 20, 20, bw, bh, "btnApproach",
                        p_( "bar", "Approach" ), bar_approach, SDLK_A );

   /* Bar description. */
   window_addText( wid, iw + 40, -40, w - iw - 60, dh, 0, "txtDescription",
                   &gl_defFont, NULL, desc );

   /* Add portrait text. */
   th = -40 - dh - 40;
   window_addText( wid, iw + 40, th, w - iw - 60, gl_defFont.h, 1,
                   "txtPortrait", &gl_defFont, NULL, NULL );

   /* Add mission description text. */
   th -= 20 + PORTRAIT_HEIGHT + 20 + 20;
   window_addText( wid, iw + 60, th, w - iw - 100, h + th - ( 2 * bh + 60 ), 0,
                   "txtMission", &gl_defFont, NULL, NULL );

   /* Generate the mission list. */
   bar_genList( wid );
}

/**
 * @brief Generates the mission list for the bar.
 *
 *    @param wid Window to create mission list for.
 */
static int bar_genList( unsigned int wid )
{
   ImageArrayCell *portraits;
   int             w, h, iw, ih, bw, bh;
   int             n, pos, marktab;

   /* Validity check. */
   if ( wid == 0 )
      return 0;

   /* Get dimensions. */
   bar_getDim( wid, &w, &h, &iw, &ih, &bw, &bh );
   marktab = 0;

   /* Destroy widget if already exists. */
   if ( widget_exists( wid, "iarMissions" ) ) {
      /* Store position. */
      pos = toolkit_getImageArrayPos( wid, "iarMissions" );
      window_destroyWidget( wid, "iarMissions" );
   } else
      pos = -1;

   /* We sort just in case. */
   npc_sort();

   /* Set up missions. */
   if ( mission_portrait == NULL )
      mission_portrait = gl_newImage( PORTRAIT_GFX_PATH "news", 0 );
   n = npc_getArraySize();
   if ( n <= 0 ) {
      n                    = 1;
      portraits            = calloc( 1, sizeof( ImageArrayCell ) );
      portraits[0].image   = gl_dupTexture( mission_portrait );
      portraits[0].caption = strdup( _( "News" ) );
   } else {
      n                    = n + 1;
      portraits            = calloc( n, sizeof( ImageArrayCell ) );
      portraits[0].image   = gl_dupTexture( mission_portrait );
      portraits[0].caption = strdup( _( "News" ) );
      for ( int i = 0; i < npc_getArraySize(); i++ ) {
         ImageArrayCell  *p  = &portraits[i + 1];
         const glTexture *bg = npc_getBackground( i );
         p->caption          = strdup( npc_getName( i ) );
         if ( bg != NULL ) {
            p->image  = gl_dupTexture( bg );
            p->layers = gl_addTexArray( p->layers,
                                        gl_dupTexture( npc_getTexture( i ) ) );
         } else
            p->image = gl_dupTexture( npc_getTexture( i ) );
         if ( npc_isImportant( i ) ) {
            p->layers = gl_addTexArray(
               p->layers,
               gl_newImage( OVERLAY_GFX_PATH "portrait_exclamation", 0 ) );
            marktab = 1;
         }
      }
   }
   window_addImageArray( wid, 20, -40, iw, ih, "iarMissions", 128, 96,
                         portraits, n, bar_update, bar_approach, bar_approach );

   /* Restore position. */
   toolkit_setImageArrayPos( wid, "iarMissions", pos );

   /* write the outfits stuff */
   bar_update( wid, NULL );

   /* Set default keyboard focus. */
   window_setFocus( wid, "iarMissions" );

   /* Determine if we want to mark the spaceport bar tab. */
   if ( marktab )
      window_tabWinSetTabName( land_wid, "tabLand",
                               land_windowsMap[LAND_WINDOW_BAR],
                               _( "Spaceport Bar #r!!#0" ) );
   else
      window_tabWinSetTabName( land_wid, "tabLand",
                               land_windowsMap[LAND_WINDOW_BAR],
                               _( "Spaceport Bar" ) );

   return 0;
}
/**
 * @brief Patches a mission into the mission computer.
 */
void misn_patchMission( const Mission *misn )
{
   array_push_back( &mission_computer, *misn );
   misn_regen();
}
/**
 * @brief Regenerates the mission list.
 */
void misn_regen( void )
{
   if ( !landed )
      return;
   if ( !land_loaded )
      return;
   if ( land_spob == NULL )
      return;
   misn_genList( land_getWid( LAND_WINDOW_MISSION ) );
}
/**
 * @brief Regenerates the bar list.
 */
void bar_regen( void )
{
   if ( !landed )
      return;
   if ( !land_loaded )
      return;
   if ( land_spob == NULL )
      return;
   NTracingZone( _ctx, 1 );
   bar_genList( land_getWid( LAND_WINDOW_BAR ) );
   NTracingZoneEnd( _ctx );
}
/**
 * @brief Updates the missions in the spaceport bar.
 *    @param wid Window to update the outfits in.
 *    @param str Unused.
 */
static void bar_update( unsigned int wid, const char *str )
{
   (void)str;
   int pos;
   int w, h, iw, ih, bw, bh, dh;

   /* Get dimensions. */
   bar_getDim( wid, &w, &h, &iw, &ih, &bw, &bh );
   dh = gl_printHeightRaw( &gl_defFont, w - iw - 60,
                           _( land_spob->bar_description ) );

   /* Get array. */
   pos = toolkit_getImageArrayPos( wid, "iarMissions" );

   /* See if is news. */
   if ( pos == 0 ) { /* News selected. */
      /* Destroy news widget if needed. */
      if ( widget_exists( wid, "cstNews" ) )
         window_destroyWidget( wid, "cstNews" );

      /* Destroy portrait. */
      if ( widget_exists( wid, "imgPortrait" ) )
         window_destroyWidget( wid, "imgPortrait" );
      if ( widget_exists( wid, "imgPortraitBG" ) )
         window_destroyWidget( wid, "imgPortraitBG" );

      /* Disable button. */
      window_disableButton( wid, "btnApproach" );

      /* Clear text. */
      window_modifyText( wid, "txtPortrait", NULL );
      window_modifyText( wid, "txtMission", NULL );

      /* Create news. */
      news_widget( wid, iw + 60, -40 - ( 40 + dh ), w - iw - 100,
                   h - 40 - ( dh + 20 ) - 40 - bh - 20 );
      return;
   }

   /* Shift to ignore news now. */
   pos--;

   /* Destroy news widget if needed. */
   if ( widget_exists( wid, "cstNews" ) )
      window_destroyWidget( wid, "cstNews" );

   /* Create widgets if needed. */
   if ( !widget_exists( wid, "imgPortraitBG" ) ) /* Must be first */
      window_addImage( wid, iw + 40 + ( w - iw - 60 - PORTRAIT_WIDTH ) / 2,
                       -( 40 + dh + 40 + gl_defFont.h + 20 + PORTRAIT_HEIGHT ),
                       0, 0, "imgPortraitBG", NULL, 1 );
   if ( !widget_exists( wid, "imgPortrait" ) )
      window_addImage( wid, iw + 40 + ( w - iw - 60 - PORTRAIT_WIDTH ) / 2,
                       -( 40 + dh + 40 + gl_defFont.h + 20 + PORTRAIT_HEIGHT ),
                       0, 0, "imgPortrait", NULL, 1 );

   /* Enable button. */
   window_enableButton( wid, "btnApproach" );

   /* Set portrait. */
   window_modifyText( wid, "txtPortrait", npc_getName( pos ) );
   window_modifyImage( wid, "imgPortrait", npc_getTexture( pos ),
                       PORTRAIT_WIDTH, PORTRAIT_HEIGHT );
   window_modifyImage( wid, "imgPortraitBG", npc_getBackground( pos ),
                       PORTRAIT_WIDTH, PORTRAIT_HEIGHT );

   /* Set mission description. */
   window_modifyText( wid, "txtMission", npc_getDesc( pos ) );
}
/**
 * @brief Closes the mission computer window.
 *    @param wid Window to close.
 *    @param name Unused.
 */
static void bar_close( unsigned int wid, const char *name )
{
   (void)wid;
   (void)name;

   /* Must not be regenerating. */
   if ( land_regen ) {
      land_regen--;
      return;
   }

   gl_freeTexture( mission_portrait );
   mission_portrait = NULL;
}
/**
 * @brief Approaches guy in mission computer.
 */
static void bar_approach( unsigned int wid, const char *str )
{
   (void)str;
   int pos, n;

   /* Get position. */
   pos = toolkit_getImageArrayPos( wid, "iarMissions" );

   /* Should never happen, but in case news is selected */
   if ( pos == 0 )
      return;

   /* Ignore news. */
   pos--;

   n = npc_getArraySize();
   npc_approach( pos );
   /* This check is necessary if the player quits the game in the middle of an
    * NPC approach. */
   if ( land_spob == NULL )
      return;
   bar_genList( wid ); /* Always just in case. */

   /* Focus the news if the number of NPCs has changed. */
   if ( n != npc_getArraySize() )
      toolkit_setImageArrayPos( wid, "iarMissions", 0 );

   /* Reset markers. */
   mission_sysMark();

   /* Mission forced take off. */
   land_needsTakeoff( 0 );
}
/**
 * @brief Loads the news.
 *
 * @return 0 on success.
 */
static int news_load( void )
{
   generate_news( land_spob->presence.faction );
   return 0;
}

/**
 * @brief Opens the mission computer window.
 */
static void misn_open( unsigned int wid )
{
   int w, h, y;

   /* Mark as generated. */
   land_tabGenerate( LAND_WINDOW_MISSION );

   /* Get window dimensions. */
   window_dimWindow( wid, &w, &h );

   /* buttons */
   int bw = MIN( LAND_BUTTON_WIDTH, ( w / 2 - 80 ) / 3 );
   window_addButtonKey( wid, -20, 20, bw, LAND_BUTTON_HEIGHT, "btnCloseMission",
                        _( "Take Off" ), land_buttonTakeoff, SDLK_T );
   window_addButtonKey( wid, -20 - bw - 20, 20, bw, LAND_BUTTON_HEIGHT,
                        "btnAcceptMission", _( "Accept Mission" ), misn_accept,
                        SDLK_A );
   window_addButtonKey( wid, -20 - 2 * ( bw + 20 ), 20, bw, LAND_BUTTON_HEIGHT,
                        "btnAutonavMission", _( "Autonav" ), misn_autonav,
                        SDLK_N );

   /* text */
   y = -60;
   window_addText( wid, w / 2 + 10, y, w / 2 - 30, 40, 0, "txtSDate", NULL,
                   &cFontGrey,
                   _( "Date:\n"
                      "Free Space:" ) );
   window_addText( wid, w / 2 + 110, y, w / 2 - 130, 40, 0, "txtDate", NULL,
                   NULL, NULL );
   y -= 2 * gl_defFont.h + 30;
   window_addText( wid, w / 2 + 10, y, w / 2 - 30, 50, 0, "txtHeader",
                   &gl_defFont, NULL, NULL );
   y -= 50;
   window_addText( wid, w / 2 + 10, y, w / 2 - 30,
                   y - 40 + h - 2 * LAND_BUTTON_HEIGHT, 0, "txtDesc",
                   &gl_defFont, NULL, NULL );

   /* map */
   map_show( wid, 20, 20, w / 2 - 30, h / 2 - 35, 0.75, 0., 0. );

   misn_genList( wid );
   space_clearComputerMarkers(); /* Don't want markers at the beginning. */
}
/**
 * @brief Autonav to selected mission.
 *    @param wid Window of the mission computer.
 *    @param str Unused.
 */
static void misn_autonav( unsigned int wid, const char *str )
{
   Mission          *misn;
   const StarSystem *sys;
   int               misn_ref;
   (void)str;

   /* Makes sure current mission has system */
   misn_ref = mission_computer_filter[toolkit_getListPos( wid, "lstMission" )];
   misn     = &mission_computer[misn_ref];
   sys      = mission_sysComputerMark( misn );
   if ( sys == NULL )
      return;

   /* Select mission's target system */
   map_select( sys, 0 );

   /* Autonav to target system */
   player_hyperspacePreempt( 1 );
   player_autonavStart();
}
/**
 * @brief Accepts the selected mission.
 *    @param wid Window of the mission computer.
 *    @param str Unused.
 */
static void misn_accept( unsigned int wid, const char *str )
{
   (void)str;
   const char *misn_name = toolkit_getList( wid, "lstMission" );

   /* Make sure you have missions. */
   if ( strcmp( misn_name, _( "No Missions" ) ) == 0 )
      return;

   if ( dialogue_YesNo(
           _( "Accept Mission" ),
           _( "Are you sure you want to accept this mission?" ) ) ) {
      int      changed  = 0;
      int      pos      = toolkit_getListPos( wid, "lstMission" );
      int      misn_ref = mission_computer_filter[pos];
      Mission *misn     = &mission_computer[misn_ref];
      int      ret      = mission_accept( misn );
      if ( ret == -1 ) { /* Errored out. */
         mission_cleanup( &mission_computer[misn_ref] );
         changed = 1;
      }
      if ( ( ret == 2 ) || ( ret == 3 ) ) /* Deleted or accepted. */
         changed = 1;

      if ( changed ) {
         array_erase( &mission_computer, &mission_computer[misn_ref],
                      &mission_computer[misn_ref + 1] );

         /* Regenerate list. */
         misn_genList( wid );
      }

      /* Reset markers. */
      mission_sysMark();
   }
}

/**
 * @brief Hides missions we don't want to look at.
 */
static int *misn_filter( const Mission *misn )
{
   int *filtered = array_create_size( int, array_size( misn ) );
   for ( int i = 0; i < array_size( misn ); i++ ) {
      if ( misn_opts.hideillegal && misn[i].illegal )
         continue;
      array_push_back( &filtered, i );
   }
   return filtered;
}

/**
 * @brief Used to sort available mission computer missions.
 */
static int misn_cmp( const void *p1, const void *p2 )
{
   int            r1 = *(const int *)p1;
   int            r2 = *(const int *)p2;
   const Mission *m1 = &mission_computer[r1];
   const Mission *m2 = &mission_computer[r2];
   credits_t      c;

   switch ( misn_opts.sortby ) {
   case MISNCOMPUTER_SORT_PRIORITY:
   default:
      /* Just fall through to default. */
      break;

   /* Case sorting by reward. */
   case MISNCOMPUTER_SORT_REWARD:
      c = m2->reward_value - m1->reward_value;
      if ( c )
         return c;
      break;

   case MISNCOMPUTER_SORT_DISTANCE:
      c = m1->distance - m2->distance;
      if ( c )
         return c;
      break;
   }

   /* Default search priority. */
   if ( m1->data->avail.priority < m2->data->avail.priority )
      return -1;
   else if ( m1->data->avail.priority > m2->data->avail.priority )
      return +1;

   return strcmp( m1->data->name, m2->data->name );
}

/**
 * @brief Opens the small pop-down menu.
 */
static void misn_popdown( unsigned int wid, const char *str )
{
   const char *name   = "lstMissionPopdown";
   const char *sort[] = {
      [MISNCOMPUTER_SORT_PRIORITY] = _( "Sort by Priority" ),
      [MISNCOMPUTER_SORT_REWARD]   = _( "Sort by Reward" ),
      [MISNCOMPUTER_SORT_DISTANCE] = _( "Sort by Distance" ),
      [MISNCOMPUTER_SORT_SETTINGS] = _( "Settings" ),
   };
   size_t n = sizeof( sort ) / sizeof( sort[0] );
   char **sortlist;
   int    x, y, w, h;

   if ( widget_exists( wid, name ) ) {
      window_destroyWidget( wid, name );
      return;
   }

   sortlist = malloc( sizeof( sort ) );
   for ( size_t i = 0; i < n; i++ )
      sortlist[i] = strdup( _( sort[i] ) );

   window_dimWidget( wid, str, &w, &h );
   window_posWidget( wid, str, &x, &y );
   window_addList( wid, x + w, y - 120 + h, 350, 120, name, sortlist, n,
                   misn_opts.sortby, misn_popdownSelect, misn_popdownActivate );
   window_setFocus( wid, name );
}

static void misn_popdownSelect( unsigned int wid, const char *str )
{
   MissionComputerSort p = toolkit_getListPos( wid, str );
   if ( p == MISNCOMPUTER_SORT_SETTINGS ) {
      misn_computerOptions( wid, str );
      window_destroyWidget( wid, str );
      return;
   }

   /* Change sort setting. */
   if ( misn_opts.sortby != p ) {
      misn_opts.sortby = p;
      misn_genList( land_getWid( LAND_WINDOW_MISSION ) );
   }
}

static void misn_popdownActivate( unsigned int wid, const char *str )
{
   misn_popdownSelect( wid, str );
   window_destroyWidget( wid, str );
}

static void misn_computerOptionsRegen( unsigned int wid, const char *str )
{
   (void)str;
   MissionComputerOptions opts = misn_opts;
   misn_opts.sortby            = toolkit_getListPos( wid, "lstSort" );
   if ( widget_exists( wid, "chkHideIllegal" ) )
      misn_opts.hideillegal = window_checkboxState( wid, "chkHideIllegal" );
   /* Only regenerate if changed. */
   if ( memcmp( &misn_opts, &opts, sizeof( MissionComputerOptions ) ) )
      misn_genList( land_getWid( LAND_WINDOW_MISSION ) );
}

/**
 * @brief Closes the mission computer window.
 *    @param wid Window to close.
 *    @param name Unused.
 */
static void misn_computerOptionsClose( unsigned int wid, const char *name )
{
   misn_computerOptionsRegen( wid, name );
   window_close( wid, name );
}

/**
 * @brief Do the popdown options.
 */
static void misn_computerOptions( unsigned int wid, const char *str )
{
   (void)wid;
   (void)str;
   int         w      = 240;
   int         h      = 300;
   const char *sort[] = {
      [MISNCOMPUTER_SORT_PRIORITY] = _( "Priority (Default)" ),
      [MISNCOMPUTER_SORT_REWARD]   = _( "Reward" ),
      [MISNCOMPUTER_SORT_DISTANCE] = _( "Distance" ),
   };
   size_t l = sizeof( sort ) / sizeof( sort[0] );
   char **sortD;
   int    mwid;
   int    y;

   /* Create the window. */
   mwid = window_create( "wdwMisnPopdown", _( "Mission Computer Settings" ), -1,
                         -1, w, h );
   window_setAccept( mwid, misn_computerOptionsClose );
   window_setCancel( mwid, misn_computerOptionsClose );

   /* Sorting. */
   y = -40;
   window_addText( mwid, 20, y, 100, h - LAND_BUTTON_HEIGHT, 0, "txtSSort",
                   &gl_defFont, NULL, _( "Sort by:" ) );
   y -= 20;
   sortD = malloc( sizeof( char * ) * l );
   for ( size_t i = 0; i < l; i++ )
      sortD[i] = strdup( sort[i] );
   window_addList( mwid, 20, y, 200, 100, "lstSort", sortD, l, 0,
                   misn_computerOptionsRegen, NULL );
   y -= 100 + 10;

   /* Filtering. */
   window_addCheckbox( mwid, 20, y, w - 40, 30, "chkHideIllegal",
                       _( "Hide Illegal Missions" ), misn_computerOptionsRegen,
                       misn_opts.hideillegal );

   /* Close button. */
   window_addButton( mwid, -20, 20, LAND_BUTTON_WIDTH, LAND_BUTTON_HEIGHT,
                     "btnClose", _( "Close" ), misn_computerOptionsClose );
}

/**
 * @brief Generates the mission list.
 *    @param wid Window to generate the mission list for.
 */
static void misn_genList( unsigned int wid )
{
   char **misn_names;
   int    j, w, h, pos;

   /* Validity check. */
   if ( wid == 0 )
      return;

   if ( widget_exists( wid, "lstMission" ) ) {
      pos = toolkit_getListPos( wid, "lstMission" );
      window_destroyWidget( wid, "lstMission" );
      window_destroyWidget( wid, "btnMissionFilter" );
   } else
      pos = -1;

   /* Get window dimensions. */
   window_dimWindow( wid, &w, &h );

   /* Resort just in case. */
   array_free( mission_computer_filter );
   mission_computer_filter = misn_filter( mission_computer );
   qsort( mission_computer_filter, array_size( mission_computer_filter ),
          sizeof( int ), misn_cmp );

   /* list */
   j          = 1; /* make sure we don't accidentally free the memory twice. */
   misn_names = NULL;
   if ( array_size( mission_computer_filter ) > 0 ) { /* there are missions */
      misn_names =
         malloc( sizeof( char * ) * array_size( mission_computer_filter ) );
      j = 0;
      for ( int i = 0; i < array_size( mission_computer_filter ); i++ ) {
         int            misn_ref = mission_computer_filter[i];
         const Mission *misn     = &mission_computer[misn_ref];
         if ( misn->title != NULL )
            misn_names[j++] = strdup( misn->title );
      }
   }
   if ( ( misn_names == NULL ) ||
        ( array_size( mission_computer_filter ) == 0 ) ||
        ( j == 0 ) ) { /* no missions. */
      if ( j == 0 )
         free( misn_names );
      misn_names    = malloc( sizeof( char * ) );
      misn_names[0] = strdup( _( "No Missions" ) );
      j             = 1;
   }
   window_addList( wid, 20, -40, w / 2 - 30, h / 2 - 35, "lstMission",
                   misn_names, j, 0, misn_update, misn_accept );

   /* Set default keyboard focus. */
   window_setFocus( wid, "lstMission" );

   /* Add position persistancey after a mission has been accepted */
   /* NOTE: toolkit_setListPos protects us from a bad position by clamping */
   toolkit_setListPos( wid, "lstMission", pos );

   /* Add popdown menu stuff. */
   window_addButton( wid, 20 + w / 2 - 30 - 25, -35, 30, 30, "btnMissionFilter",
                     NULL, misn_popdown );
   window_buttonCustomRender( wid, "btnMissionFilter",
                              window_buttonCustomRenderGear );
}
/**
 * @brief Updates the mission list.
 *    @param wid Window of the mission computer.
 *    @param str Unused.
 */
static void misn_update( unsigned int wid, const char *str )
{
   (void)str;
   const char       *active_misn;
   Mission          *misn;
   int               misn_ref;
   const StarSystem *sys;
   char              txt[STRMAX_SHORT], *buf;
   int               cargofree = pfleet_cargoMissionFree();

   /* Clear computer markers. */
   space_clearComputerMarkers();

   /* Update date stuff. */
   buf = ntime_pretty( 0, 2 );
   snprintf( txt, sizeof( txt ),
             n_( "%s\n%d Tonne", "%s\n%d Tonnes", cargofree ), buf, cargofree );
   free( buf );
   window_modifyText( wid, "txtDate", txt );

   active_misn = toolkit_getList( wid, "lstMission" );
   if ( strcmp( active_misn, _( "No Missions" ) ) == 0 ) {
      window_modifyText( wid, "txtHeader", NULL );
      window_modifyText( wid, "txtDesc",
                         _( "There are no missions available here." ) );
      window_disableButton( wid, "btnAcceptMission" );
      window_disableButton( wid, "btnAutonavMission" );
      return;
   }

   misn_ref = mission_computer_filter[toolkit_getListPos( wid, "lstMission" )];
   misn     = &mission_computer[misn_ref];
   sys      = mission_sysComputerMark( misn );
   if ( sys != NULL )
      map_center( wid, sys->name );
   snprintf( txt, sizeof( txt ), _( "%s\n#nReward:#0 %s" ), misn->title,
             misn->reward );
   window_modifyText( wid, "txtHeader", txt );
   window_modifyText( wid, "txtDesc", misn->desc );
   window_enableButton( wid, "btnAcceptMission" );
   window_enableButton( wid, "btnAutonavMission" );
}

/**
 * @brief Refuels the player's current ship, if possible.
 */
void land_refuel( void )
{
   unsigned int w;

   /* Full fuel. */
   if ( player.p->fuel >= player.p->fuel_max )
      return;

   /* No refuel service. */
   if ( ( land_spob == NULL ) ||
        !spob_hasService( land_spob, SPOB_SERVICE_REFUEL ) )
      return;

   player.p->fuel = player.p->fuel_max;

   w = land_getWid( LAND_WINDOW_EQUIPMENT );
   if ( w > 0 )
      equipment_updateShips( w, NULL ); /* Must update counter. */
}

/**
 * @brief Buys a local system map.
 */
static void spaceport_buyMap( unsigned int wid, const char *str )
{
   (void)wid;
   (void)str;
   const Outfit *o = outfit_get( LOCAL_MAP_NAME );
   unsigned int  w;

   if ( o == NULL ) {
      WARN( _( "Outfit '%s' does not exist!" ), LOCAL_MAP_NAME );
      return;
   }

   /* Make sure the map isn't already known, etc. */
   if ( !outfit_canBuy( o, -1 ) ) {
      land_errDisplay();
      return;
   }

   player_modCredits( -outfit_price( o ) );
   player_addOutfit( o, 1 );

   /* Disable the button. */
   window_disableButtonSoft( land_windows[0], "btnMap" );

   /* Update map quantity in outfitter. */
   w = land_getWid( LAND_WINDOW_OUTFITS );
   if ( w > 0 )
      outfits_regenList( w, NULL );

   /* Update main tab. */
   land_updateMainTab();
}

/**
 * @brief Adds the "Buy Local Map" button if needed and updates info.
 */
void land_updateMainTab( void )
{
   char          buf[STRMAX], cred[ECON_CRED_STRLEN], tons[STRMAX_SHORT];
   size_t        l = 0;
   const Outfit *o;

   /* Update credits. */
   tonnes2str( tons, player.p->cargo_free );
   credits2str( cred, player.p->credits, 2 );
   l += scnprintf( &buf[l], sizeof( buf ) - l, _( "%s (%s system)" ),
                   spob_name( land_spob ), _( cur_system->name ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n" );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "%s",
                   space_className( land_spob ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n%s",
                   land_spob->presence.faction >= 0
                      ? _( faction_name( land_spob->presence.faction ) )
                      : _( "None" ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n%s",
                   space_populationStr( land_spob ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n\n%s", tons );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n%s", cred );
   /* Show tags. */
   if ( conf.devmode ) {
      l += scnprintf( &buf[l], sizeof( buf ) - l, "\n" );
      for ( int i = 0; i < array_size( land_spob->tags ); i++ )
         l += scnprintf( &buf[l], sizeof( buf ) - l, "%s%s",
                         ( ( i > 0 ) ? ", " : "" ), land_spob->tags[i] );
   }

   window_modifyText( land_windows[0], "txtDInfo", buf );

   /* Make sure maps are available. */
   if ( !land_hasLocalMap() )
      return;

   if ( LOCAL_MAP_NAME != NULL ) {
      o = outfit_get( LOCAL_MAP_NAME );
      if ( o == NULL ) {
         WARN( _( "Outfit '%s' does not exist!" ), LOCAL_MAP_NAME );
         return;
      }
      if ( !outfit_isLocalMap( o ) ) {
         WARN( _( "Outfit '%s' is not a local map!" ), LOCAL_MAP_NAME );
         return;
      }
   } else
      return;

   /* Just enable button if it exists. */
   if ( widget_exists( land_windows[0], "btnMap" ) )
      window_enableButton( land_windows[0], "btnMap" );
   /* Else create it. */
   else {
      /* Buy local map button. */
      credits2str( cred, outfit_price( o ), 0 );
      snprintf( buf, sizeof( buf ), _( "Buy Local Map (%s)" ), cred );
      window_addButtonKey( land_windows[0], -20 - LAND_BUTTON_WIDTH - 20, 20,
                           LAND_BUTTON_WIDTH, LAND_BUTTON_HEIGHT, "btnMap", buf,
                           spaceport_buyMap, SDLK_B );
   }

   /* Make sure player can click it. */
   if ( !outfit_canBuy( o, -1 ) )
      window_disableButtonSoft( land_windows[0], "btnMap" );
}

/**
 * @brief Wrapper for takeoff mission button.
 *
 *    @param wid Window causing takeoff.
 *    @param unused Unused.
 */
void land_buttonTakeoff( unsigned int wid, const char *unused )
{
   (void)wid;
   (void)unused;
   /* We'll want the time delay. */
   takeoff( 1, player_isFlag( PLAYER_NOSAVE ) );
}

/**
 * @brief Cleans up the land window.
 *
 *    @param wid Window closing.
 *    @param name Unused.
 */
static void land_cleanupWindow( unsigned int wid, const char *name )
{
   (void)wid;
   (void)name;

   /* Must not be regenerating. */
   if ( land_regen ) {
      land_regen--;
      return;
   }

   /* Clean up possible stray graphic. */
   if ( gfx_exterior != NULL ) {
      gl_freeTexture( gfx_exterior );
      gfx_exterior = NULL;
   }
}

/**
 * @brief Gets the WID of a window by type.
 *
 *    @param window Type of window to get wid (LAND_WINDOW_MAIN, ...).
 *    @return 0 on error, otherwise the wid of the window.
 */
unsigned int land_getWid( int window )
{
   if ( land_windowsMap[window] == -1 )
      return 0;
   return land_windows[land_windowsMap[window]];
}

/**
 * @brief Sets up the tabs for the window.
 */
static void land_setupTabs( void )
{
   int         j;
   const char *names[LAND_NUMWINDOWS];

   /* Destroy if exists. */
   if ( widget_exists( land_wid, "tabLand" ) )
      window_destroyWidget( land_wid, "tabLand" );

   /* Set window map to invalid. */
   for ( int i = 0; i < LAND_NUMWINDOWS; i++ )
      land_windowsMap[i] = -1;

   /* See what is available. */
   j = 0;
   /* Main. */
   land_windowsMap[LAND_WINDOW_MAIN] = j;
   names[j++]                        = _( "Landing Main" );
   /* Bar. */
   if ( spob_hasService( land_spob, SPOB_SERVICE_BAR ) ) {
      land_windowsMap[LAND_WINDOW_BAR] = j;
      names[j++]                       = _( "Spaceport Bar" );
   }
   /* Missions. */
   if ( spob_hasService( land_spob, SPOB_SERVICE_MISSIONS ) ) {
      land_windowsMap[LAND_WINDOW_MISSION] = j;
      names[j++]                           = _( "Missions" );
   }
   /* Outfits. */
   if ( spob_hasService( land_spob, SPOB_SERVICE_OUTFITS ) ) {
      land_windowsMap[LAND_WINDOW_OUTFITS] = j;
      names[j++]                           = _( "Outfits" );
   }
   /* Shipyard. */
   if ( spob_hasService( land_spob, SPOB_SERVICE_SHIPYARD ) ) {
      land_windowsMap[LAND_WINDOW_SHIPYARD] = j;
      names[j++]                            = _( "Shipyard" );
   }
   /* Equipment. */
   if ( spob_hasService( land_spob, SPOB_SERVICE_REFUEL ) ||
        spob_hasService( land_spob, SPOB_SERVICE_OUTFITS ) ||
        spob_hasService( land_spob, SPOB_SERVICE_SHIPYARD ) ) {
      land_windowsMap[LAND_WINDOW_EQUIPMENT] = j;
      names[j++]                             = p_( "service", "Equipment" );
   }
   /* Commodity. */
   if ( spob_hasService( land_spob, SPOB_SERVICE_COMMODITY ) ) {
      land_windowsMap[LAND_WINDOW_COMMODITY] = j;
      names[j++]                             = _( "Commodity" );
   }

   land_windows = window_addTabbedWindow( land_wid, -1, -1, -1, -1, "tabLand",
                                          j, names, 0 );
}

/**
 * @brief Recreates the land windows.
 *
 *    @param load Is loading game?
 */
void land_genWindows( int load )
{
   int          w, h;
   Spob        *p;
   int          regen;
   unsigned int pntservices;

   NTracingZone( _ctx, 1 );

   /* Destroy old window if exists. */
   if ( land_wid > 0 ) {
      land_regen = 2; /* Mark we're regenning. */
      window_destroy( land_wid );

      /* Mark tabs as not generated. */
      land_generated = 0;
   }
   land_loaded = 0;

   /* Get spob. */
   p           = land_spob;
   regen       = landed;
   pntservices = p->services;

   /* Create window. */
   if ( SCREEN_W < LAND_WIDTH || SCREEN_H < LAND_HEIGHT ) {
      w = -1; /* Fullscreen. */
      h = -1;
   } else {
      w = LAND_WIDTH + 0.5 * ( SCREEN_W - LAND_WIDTH );
      h = LAND_HEIGHT + 0.5 * ( SCREEN_H - LAND_HEIGHT );
   }
   land_wid = window_create( "wdwLand", spob_name( p ), -1, -1, w, h );
   window_onClose( land_wid, land_cleanupWindow );

   /* Create tabbed window. */
   land_setupTabs();

   /* Have to be set _BEFORE_ the main tabe is created. */
   if ( !regen )
      landed = 1;

   /*
    * Order here is very important:
    *
    *  1) Create main tab - must have decent background.
    *  2) Set landed, play music and run land hooks - so hooks run well.
    *  3) Generate missions - so that campaigns are fluid.
    *  4) Create other tabs - lists depend on NPC and missions.
    */
   /* 1) Create main tab. */
   land_createMainTab( land_getWid( LAND_WINDOW_MAIN ) );

   /* Add local system map button. */
   land_updateMainTab();

   /* 2) Set as landed and run hooks. */
   if ( !regen ) {
      music_choose(
         "land" ); /* Must be before hooks in case hooks change music. */

      NTracingZoneName( _ctx_landhooks, "hooks_run(\"land\")", 1 );
      /* We don't run the "land" hook when loading. If you want to have it do
       * stuff when loading, use the "load" hook. Note that you can use the same
       * function for both hooks. */
      if ( !load )
         hooks_run( "land" );
      else
         hooks_run(
            "load" ); /* Should be run before generating missions, so if the
                         load hook cancels a mission, it can reappear. */
      NTracingZoneEnd( _ctx_landhooks );
      events_trigger( EVENT_TRIGGER_LAND );

      /* An event, hook or the likes made Naev quit. */
      if ( naev_isQuit() ) {
         NTracingZoneEnd( _ctx );
         return;
      }

      /* Make sure services didn't change or we have to do the tab window. */
      if ( land_spob->services != pntservices ) {
         land_setupTabs();
         land_createMainTab( land_getWid( LAND_WINDOW_MAIN ) );
         land_updateMainTab();
      }

      /* 3) Generate computer and bar missions. */
      /* Generate bar missions first for claims. */
      if ( spob_hasService( land_spob, SPOB_SERVICE_BAR ) )
         npc_generateMissions(); /* Generate bar npc. */
      if ( spob_hasService( land_spob, SPOB_SERVICE_MISSIONS ) )
         mission_computer =
            missions_genList( land_spob->presence.faction, land_spob,
                              cur_system, MIS_AVAIL_COMPUTER );
   }

   /* 4) Create other tabs. */
#define should_open( s, w )                                                    \
   ( spob_hasService( land_spob, s ) && ( !land_tabGenerated( w ) ) )

   /* Things get a bit hairy here. Hooks may have triggered a GUI reload via
    * e.g. player.swapShip, so the land tabs may have been generated already
    * and we need to check that before regenerating them.
    */

   /* Basic - bar + missions */
   if ( should_open( SPOB_SERVICE_BAR, LAND_WINDOW_BAR ) )
      bar_open( land_getWid( LAND_WINDOW_BAR ) );
   if ( should_open( SPOB_SERVICE_MISSIONS, LAND_WINDOW_MISSION ) )
      misn_open( land_getWid( LAND_WINDOW_MISSION ) );
   /* Outfits. */
   if ( should_open( SPOB_SERVICE_OUTFITS, LAND_WINDOW_OUTFITS ) )
      outfits_open( land_getWid( LAND_WINDOW_OUTFITS ), NULL,
                    spob_hasService( land_spob, SPOB_SERVICE_BLACKMARKET ) );
   /* Shipyard. */
   if ( should_open( SPOB_SERVICE_SHIPYARD, LAND_WINDOW_SHIPYARD ) )
      shipyard_open( land_getWid( LAND_WINDOW_SHIPYARD ) );
   /* Equipment. */
   if ( ( spob_hasService( land_spob, SPOB_SERVICE_REFUEL ) ||
          spob_hasService( land_spob, SPOB_SERVICE_OUTFITS ) ||
          spob_hasService( land_spob, SPOB_SERVICE_SHIPYARD ) ) &&
        !land_tabGenerated( LAND_WINDOW_EQUIPMENT ) )
      equipment_open( land_getWid( LAND_WINDOW_EQUIPMENT ) );
   /* Commodity. */
   if ( should_open( SPOB_SERVICE_COMMODITY, LAND_WINDOW_COMMODITY ) )
      commodity_exchange_open( land_getWid( LAND_WINDOW_COMMODITY ) );
#undef should_open

   if ( !regen ) {
      /* Reset markers if needed. */
      mission_sysMark();

      /* Check land missions. */
      if ( !has_visited( VISITED_LAND ) ) {
         missions_run( MIS_AVAIL_LAND, land_spob->presence.faction, land_spob,
                       cur_system );
         visited( VISITED_LAND );
      }
   }

   /* Go to last open tab. */
   window_tabWinOnChange( land_wid, "tabLand", land_changeTab );
   if ( !land_takeoff && land_windowsMap[last_window] != -1 )
      window_tabWinSetActive( land_wid, "tabLand",
                              land_windowsMap[last_window] );

   /* Refresh the map button in case the player couldn't afford it prior to
    * mission payment. */
   land_updateMainTab();

   /* Refuel if necessary. */
   land_refuel();

   /* Finished loading. */
   land_loaded = 1;

   /* Necessary if player.land() was run in an abort() function. */
   if ( !load )
      window_lower( land_wid );

   NTracingZoneEnd( _ctx );
}

/**
 * @brief Sets the land window tab.
 *
 *    @param window Tab to set like LAND_WINDOW_COMMODITY.
 *    @return 0 on success.
 */
int land_setWindow( int window )
{
   if ( land_windowsMap[window] < 0 )
      return -1;
   last_window = window;
   window_tabWinSetActive( land_wid, "tabLand", land_windowsMap[window] );
   return 0;
}

/**
 * @brief Opens up all the land dialogue stuff.
 *    @param p Spob to open stuff for.
 *    @param load Whether or not loading the game.
 */
void land( Spob *p, int load )
{
   /* Do not land twice. */
   if ( landed )
      return;

#if HAVE_TRACY
   char   buf[STRMAX_SHORT];
   size_t l = snprintf( buf, sizeof( buf ), "Player landed on '%s'", p->name );
   NTracingMessage( buf, l );
#endif /* HAVE_TRACY */
   NTracingFrameMarkStart( "land" );

   /* Increment times player landed. */
   if ( !load ) {
      player.landed_times++;
      player.ps.landed_times++;
   }
   /* Clear landing since we landed. */
   pilot_rmFlag( player.p, PILOT_LANDING );

   /* Clear some unnecessary flags. */
   pilot_rmFlag( player.p, PILOT_COOLDOWN_BRAKE );
   pilot_rmFlag( player.p, PILOT_COOLDOWN );

   /* Heal the player so GUI shows player at full everything. */
   pilot_healLanded( player.p );

   player_addEscorts(); /* TODO only regenerate fleet if planet has a shipyard
                         */

   /* So the issue is that spfxL use the parents environment, which can be from
    * a mission or event. They usually get cleared when landing, which can
    * cause the spfx to be stale and floating until the player takes off.
    * TODO fix this shit with Rust. */
   spfxL_clear();

   /* Stop player sounds. */
   player_soundStop();

   /* Clear message stuff. */
   free( player.p->comm_msg );
   player.p->comm_msg = NULL;

   /* Clear some target stuff. */
   player.p->nav_spob = -1;
   gui_setNav();
   cam_vel( 0., 0. );

   /* Load stuff */
   land_spob    = p;
   gfx_exterior = gl_newImage( p->gfx_exterior, 0 );

   /* Run outfits as necessary. */
   pilot_outfitLOnland( player.p );

   /* Generate the news. */
   if ( spob_hasService( land_spob, SPOB_SERVICE_BAR ) )
      news_load();

   /* Average economy prices that player has seen */
   economy_averageSeenPrices( p );

   /* Clear the NPC. */
   npc_clear();

   /* Create all the windows. */
   land_genWindows( load );

   /* Just in case? */
   bar_regen();

   /* Do a lua collection pass. Run in a hook since land can be called
    * indirectly from Lua. */
   hook_addFunc( land_gc, NULL, "safe" );

   /* Mission forced take off. */
   land_needsTakeoff( 0 );

   NTracingFrameMarkEnd( "land" );
}

/**
 * @brief Runs Lua garbage collection.
 */
static int land_gc( void *unused )
{
   (void)unused;
   lua_gc( naevL, LUA_GCCOLLECT, 0 );
   return 0;
}

/**
 * @brief Creates the main tab.
 *
 *    @param wid Window to create main tab.
 */
static void land_createMainTab( unsigned int wid )
{
   int    offset;
   int    w, h, y, th;
   char   buf[STRMAX_SHORT];
   size_t l = 0;

   /* Get window dimensions. */
   window_dimWindow( wid, &w, &h );

   /*
    * Faction logo.
    */
   offset = 20;
   if ( land_spob->presence.faction != -1 ) {
      const glTexture *logo = faction_logo( land_spob->presence.faction );
      if ( logo != NULL ) {
         int logow = tex_w( logo ) * (double)FACTION_LOGO_SM /
                     MAX( tex_w( logo ), tex_h( logo ) );
         int logoh = tex_h( logo ) * (double)FACTION_LOGO_SM /
                     MAX( tex_w( logo ), tex_h( logo ) );
         window_addImage( wid, 440 + ( w - 460 - logow ) / 2, -20, logow, logoh,
                          "imgFaction", logo, 0 );
         offset += FACTION_LOGO_SM;
      }
   }

   /*
    * Pretty display.
    */
   window_addImage( wid, 20, -40, 400, 400, "imgSpob", gfx_exterior, 1 );
   if ( land_spob->description != NULL )
      window_addText( wid, 440, -20 - offset, w - 460,
                      h - 20 - offset - 60 - LAND_BUTTON_HEIGHT * 2, 0,
                      "txtSpobDesc", &gl_defFont, NULL,
                      _( land_spob->description ) );

   /* Player stats. */
   l += scnprintf( &buf[l], sizeof( buf ) - l, "#n%s", _( "Stationed at:" ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n%s", _( "Class:" ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n%s", _( "Faction:" ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n%s", _( "Population:" ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n\n%s", _( "Free Space:" ) );
   l += scnprintf( &buf[l], sizeof( buf ) - l, "\n%s", _( "Money:" ) );
   if ( conf.devmode )
      l += scnprintf( &buf[l], sizeof( buf ) - l, "\n%s", _( "Tags:" ) );
   th = gl_printHeightRaw( &gl_defFont, 200, buf );
   window_addText( wid, 20, 20, 200, th, 0, "txtSInfo", &gl_defFont, NULL,
                   buf );
   window_addText( wid, 20 + 120, 20, w - 20 - ( 20 + 200 ) - LAND_BUTTON_WIDTH,
                   th, 0, "txtDInfo", &gl_defFont, NULL, NULL );

   /*
    * buttons
    */
   /* first column */
   window_addButtonKey( wid, -20, 20, LAND_BUTTON_WIDTH, LAND_BUTTON_HEIGHT,
                        "btnTakeoff", _( "Take Off" ), land_buttonTakeoff,
                        SDLK_T );

   /* Additional notices if necessary. */
   l      = 0;
   buf[0] = '\0';
   y      = 20 + ( LAND_BUTTON_HEIGHT + 20 ) + 20;
   if ( land_hasLocalMap() )
      y += LAND_BUTTON_HEIGHT + 20;
   /* Add "no refuelling" notice if needed. */
   if ( !spob_hasService( land_spob, SPOB_SERVICE_REFUEL ) )
      l += scnprintf( &buf[l], sizeof( buf ) - l,
                      _( "No refuelling services." ) );
   if ( !land_canSave() ) {
      if ( l > 0 )
         l += scnprintf( &buf[l], sizeof( buf ) - l, "\n" );
      l += scnprintf( &buf[l], sizeof( buf ) - l, "#r%s#0",
                      _( "Game will not be saved." ) );
   }
   if ( l > 0 ) {
      th = gl_printHeightRaw( &gl_defFont, 200, buf );
      window_addText( land_windows[0], -20, y, 200, th, 0, "txtPlayer",
                      &gl_defFont, NULL, buf );
   }
}

/**
 * @brief Saves the last place the player was.
 *
 *    @param wid Unused.
 *    @param wgt Unused.
 *    @param old Previously-active tab. (Unused)
 *    @param tab Tab changed to.
 */
static void land_changeTab( unsigned int wid, const char *wgt, int old,
                            int tab )
{
   (void)wid;
   (void)wgt;
   (void)old;
   unsigned int w;
   /* Safe defaults. */
   const char  *torun_hook = NULL;
   unsigned int to_visit   = 0;

   /* Clear markers when not open. */
   if ( last_window == LAND_WINDOW_MISSION )
      space_clearComputerMarkers();

   /* Find what switched. */
   for ( int i = 0; i < LAND_NUMWINDOWS; i++ ) {
      if ( land_windowsMap[i] != tab )
         continue;
      last_window = i;
      w           = land_getWid( i );

      /* Must regenerate outfits. */
      switch ( i ) {
      case LAND_WINDOW_MAIN:
         land_updateMainTab();
         break;
      case LAND_WINDOW_OUTFITS:
         outfits_update( w, NULL );
         to_visit   = VISITED_OUTFITS;
         torun_hook = "outfits";
         break;
      case LAND_WINDOW_SHIPYARD:
         shipyard_update( w, NULL );
         to_visit   = VISITED_SHIPYARD;
         torun_hook = "shipyard";
         break;
      case LAND_WINDOW_BAR:
         bar_update( w, NULL );
         to_visit   = VISITED_BAR;
         torun_hook = "bar";
         break;
      case LAND_WINDOW_MISSION:
         misn_update( w, NULL );
         to_visit   = VISITED_MISSION;
         torun_hook = "mission";
         break;
      case LAND_WINDOW_COMMODITY:
         commodity_update( w, NULL );
         to_visit   = VISITED_COMMODITY;
         torun_hook = "commodity";
         break;
      case LAND_WINDOW_EQUIPMENT:
         equipment_updateShips( w, NULL );
         equipment_updateOutfits( w, NULL );
         to_visit   = VISITED_EQUIPMENT;
         torun_hook = "equipment";
         break;

      default:
         break;
      }

      /* Clear markers if closing Mission Computer. */
      if ( i != LAND_WINDOW_MISSION )
         space_clearComputerMarkers();

      break;
   }

   /* Check land missions - always run hooks. */
   /*if ((to_visit != 0) && !has_visited(to_visit)) {*/
   {
      /* Run hooks, run after music in case hook wants to change music. */
      if ( torun_hook != NULL )
         if ( hooks_run( torun_hook ) > 0 )
            bar_genList( land_getWid( LAND_WINDOW_BAR ) );

      visited( to_visit );

      if ( land_loaded )
         land_needsTakeoff( 1 );
   }
}

/**
 * @brief Makes the player take off if landed.
 *
 *    @param delay Whether or not to have time pass as if the player landed
 * normally.
 *    @param nosave Whether or not to avoid saving the game.
 */
void takeoff( int delay, int nosave )
{
   int         h;
   char       *nt;
   double      a, r;
   const Spob *spb = land_spob;

   if ( !landed )
      return;

   /* Check to see if player fleet is ok. */
   if ( player.fleet_capacity > 0 ) {
      char          badfleet_ships[STRMAX_SHORT];
      char          overfleet_ships[STRMAX_SHORT];
      int           l         = 0;
      int           badfleet  = 0;
      int           nships    = 0;
      int           overfleet = 0;
      int           capused   = player.p->ship->points;
      PlayerShip_t *pships    = (PlayerShip_t *)player_getShipStack();

      /* Check to see if player's fleet is OK and count ships. */
      pfleet_update();
      badfleet_ships[0] = '\0';
      for ( int i = 0; i < array_size( pships ); i++ ) {
         const PlayerShip_t *pe = &pships[i];
         if ( !pe->deployed )
            continue;
         capused += pe->p->ship->points;
         if ( capused > player.fleet_capacity ) {
            overfleet = 1;
            l += scnprintf( &overfleet_ships[l], sizeof( overfleet_ships ) - l,
                            "\n%s (%s)", pe->p->name, _( pe->p->ship->name ) );
            capused -= pe->p->ship->points;
         }
         if ( !pilot_isSpaceworthy( pe->p ) ) {
            badfleet = 1;
            l += scnprintf( &badfleet_ships[l], sizeof( badfleet_ships ) - l,
                            "\n%s (%s)", pe->p->name, _( pe->p->ship->name ) );
         }
         nships++;
      }
      /* Only care if the player has a fleet deployed. */
      if ( nships > 0 ) {
         if ( player.fleet_used > player.fleet_capacity ) {
            if ( overfleet ) {
               char buf[STRMAX];
               snprintf( buf, sizeof( buf ), "%s\n%s",
                         _( "You lack the fleet capacity to take off with all "
                            "the selected ships. Do you wish to undeploy the "
                            "following ships to be able to take off?" ),
                         overfleet_ships );
               if ( !dialogue_YesNo( _( "Fleet not fit for flight" ), "%s",
                                     buf ) )
                  return;

               capused = player.p->ship->points;
               for ( int i = 0; i < array_size( pships ); i++ ) {
                  PlayerShip_t *pe = &pships[i];
                  if ( !pe->deployed )
                     continue;
                  if ( capused + pe->p->ship->points > player.fleet_capacity ) {
                     pfleet_toggleDeploy( pe, 0 );
                     continue;
                  }
                  capused += pe->p->ship->points;
               }
            }
         }
         if ( badfleet ) {
            if ( !dialogue_YesNo( _( "Fleet not fit for flight" ), "%s\n%s",
                                  _( "The following ships in your fleet are "
                                     "not space worthy, are you sure you want "
                                     "to take off without them?" ),
                                  badfleet_ships ) )
               return;
         }
      }
   }

   /* Player's ship is not able to fly. */
   if ( !pilot_isSpaceworthy( player.p ) ) {
      char message[STRMAX_SHORT];
      pilot_reportSpaceworthy( player.p, message, sizeof( message ) );
      dialogue_msgRaw( _( "Ship not fit for flight" ), message );

      /* Check whether the player needs rescuing. */
      land_stranded();

      return;
   }

   /* Clear queued takeoff. */
   land_takeoff        = 0;
   land_takeoff_nosave = 0;

   /* Refuel if needed. */
   land_refuel();

   /* In case we had paused messy sounds. */
   sound_stopAll();

   /* ze music */
   music_choose( "takeoff" );

   /* to randomize the takeoff a bit */
   a = RNGF() * 2. * M_PI;
   r = RNGF() * land_spob->radius;

   /* no longer authorized to land */
   player_rmFlag( PLAYER_LANDACK );
   pilot_rmFlag( player.p, PILOT_LANDING ); /* No longer landing. */

   /* set player to another position with random facing direction and no vel */
   player_warp( land_spob->pos.x + r * cos( a ),
                land_spob->pos.y + r * sin( a ) );
   vec2_pset( &player.p->solid.vel, 0., 0. );
   player.p->solid.dir = RNGF() * 2. * M_PI;
   cam_setTargetPilot( player.p->id, 0 );

   /* Clear spob target. Allows for easier autonav out of the system. */
   player_targetSpobSet( -1 );

   /* Clear pilots other than player. */
   pilots_clean( 1 );

   /* Clear omsg. */
   omsg_cleanup();

   /* initialize the new space */
   h = player.p->nav_hyperspace;
   space_init( NULL, 1 );
   player.p->nav_hyperspace = h;

   /* Only save when the player shouldn't get stranded. */
   if ( !nosave && land_canSave() &&
        ( save_all() < 0 ) ) /* must be before cleaning up spob */
      dialogue_alert(
         _( "Failed to save game! You should exit and check the log to see "
            "what happened and then file a bug report!" ) );

   /* time goes by, triggers hook before takeoff */
   if ( delay ) {
      /* TODO should this depend on something else? */
      int stu = (int)( NT_PERIOD_SECONDS * player.p->stats.land_delay );
      ntime_inc( ntime_create( 0, 0, stu ) );
   }
   nt = ntime_pretty( 0, 2 );
   player_message( _( "#oTaking off from %s on %s." ), spob_name( land_spob ),
                   nt );
   free( nt );

   /* Hooks and stuff. */
   land_cleanup();         /* Cleanup stuff */
   hooks_run( "takeoff" ); /* Must be run after cleanup since we don't want the
                              missions to think we are landed. */
   if ( menu_isOpen( MENU_MAIN ) )
      return;

   /* Add escorts and heal up. */
   player_addEscorts(); /* TODO only regenerate fleet if planet has a shipyard
                         */
   effect_clear( &player.p->effects );
   pilot_healLanded( player.p );

   hooks_run( "enter" );
   if ( menu_isOpen( MENU_MAIN ) )
      return;
   events_trigger( EVENT_TRIGGER_ENTER );
   missions_run( MIS_AVAIL_ENTER, -1, NULL, NULL );
   if ( menu_isOpen( MENU_MAIN ) )
      return;

   /* Clear effects of all surviving pilots. */
   Pilot *const *plts = pilot_getAll();
   for ( int i = 0; i < array_size( plts ); i++ ) {
      Pilot *p = plts[i];
      if ( pilot_isFlag( p, PILOT_DELETE ) )
         continue;

      effect_clear( &p->effects );
      pilot_calcStats( p );

      /* Update lua stuff. */
      pilot_outfitLInitAll( p );

      /* Normal pilots stop here. */
      if ( !pilot_isWithPlayer( p ) )
         continue;

      pilot_outfitLOntakeoff( p, spb );

      /* Set take off stuff. */
      p->landing_delay = PILOT_TAKEOFF_DELAY * player_dt_default();
      p->ptimer        = p->landing_delay;
      pilot_setFlag( p, PILOT_TAKEOFF );
      pilot_setAccel( p, 0. );
      pilot_setTurn( p, 0. );
   }

   /* Reset speed */
   player_autonavResetSpeed();

   /* Landing is a special case where the player's gear can change and trigger
    * all sorts of things. We have to refresh the GUI to reflect those changes.
    * This is particular important for Lua-side mechanics such as flow. */
   gui_setSystem();

#if HAVE_TRACY
   char   buf[STRMAX_SHORT];
   size_t l =
      snprintf( buf, sizeof( buf ), "Player took off from '%s'", spb->name );
   NTracingMessage( buf, l );
#endif /* HAVE_TRACY */
}

/**
 * @brief Runs the rescue script if players are stuck.
 */
static void land_stranded( void )
{
   /* Nothing to do if there's no rescue script. */
   if ( !PHYSFS_exists( RESCUE_PATH ) )
      return;

   if ( rescue_env == NULL ) {
      char  *buf;
      size_t bufsize;

      rescue_env = nlua_newEnv( RESCUE_PATH );
      nlua_loadStandard( rescue_env );
      nlua_loadTk( rescue_env );

      buf = ndata_read( RESCUE_PATH, &bufsize );
      if ( buf == NULL ) {
         WARN( _( "File '%s' not found!" ), RESCUE_PATH );
         return;
      }
      if ( nlua_dobufenv( rescue_env, buf, bufsize, RESCUE_PATH ) != 0 ) {
         WARN( _( "Error loading file: %s\n"
                  "%s\n"
                  "Most likely Lua file has improper syntax, please check" ),
               RESCUE_PATH, lua_tostring( naevL, -1 ) );
         free( buf );
         return;
      }
      free( buf );
   }

   /* Run Lua. */
   nlua_getenv( naevL, rescue_env, "rescue" );
   if ( nlua_pcall( rescue_env, 0, 0 ) ) { /* error has occurred */
      WARN( _( "Rescue: '%s' : '%s'" ), "rescue",
            luaL_tolstring( naevL, -1, NULL ) );
      lua_pop( naevL, 1 );
   }
}

/**
 * @brief Cleans up some land-related variables.
 */
void land_cleanup( void )
{
   /* Clean up default stuff. */
   land_regen     = 0;
   land_spob      = NULL;
   landed         = 0;
   land_visited   = 0;
   land_generated = 0;

   /* Destroy window. */
   if ( land_wid > 0 )
      window_destroy( land_wid );
   land_wid = 0;

   /* Clean up possible stray graphic. */
   if ( gfx_exterior != NULL )
      gl_freeTexture( gfx_exterior );
   gfx_exterior = NULL;

   /* Remove computer markers just in case. */
   space_clearComputerMarkers();

   /* Clean up mission computer. */
   for ( int i = 0; i < array_size( mission_computer ); i++ )
      mission_cleanup( &mission_computer[i] );
   array_free( mission_computer );
   mission_computer = NULL;
   array_free( mission_computer_filter );
   mission_computer_filter = NULL;

   /* Clean up bar missions. */
   npc_clear();

   /* Clean up shipyard. */
   shipyard_cleanup();

   /* Clean up rescue Lua. */
   nlua_freeEnv( rescue_env );
   rescue_env = NULL;

   /* Deselect stuff. */
   equipment_slotDeselect( NULL );
}

/**
 * @brief Exits all the landing stuff.
 */
void land_exit( void )
{
   land_cleanup();
   equipment_cleanup();
   outfits_cleanup();
   commodity_exchange_cleanup();
}
